#!/usr/bin/env python3
import re
import sys
from pathlib import Path

def parse_map_text(text: str):
    flash_used = 0
    sram_data = 0
    sram_bss = 0
    heap_sz = 0
    sram_extra = 0
    flash_total = 0
    sram_total = 0
    stack_total = 0

    sec_re = re.compile(r"^\s*\.([A-Za-z0-9_]+)\s+0x([0-9A-Fa-f]+)\s+0x([0-9A-Fa-f]+)")
    # Older/other map formats sometimes show "LENGTH = 0x..."; retain as fallback
    length_re_flash = re.compile(r"^\s*int_flash.*LENGTH\s*=\s*0x([0-9A-Fa-f]+)")
    length_re_sram  = re.compile(r"^\s*int_sram(?!_).*(?:LENGTH)\s*=\s*0x([0-9A-Fa-f]+)")
    length_re_stack = re.compile(r"^\s*int_sram_stack_c0.*LENGTH\s*=\s*0x([0-9A-Fa-f]+)")
    # GNU ld map usually has a "Memory Configuration" table with lines like:
    #   int_flash        0x00000410         0x0017fbf0
    #   int_sram         0x1ffe0100         0x0003df00
    #   int_sram_stack_c0 0x2001e000        0x00001000
    memcfg_re = re.compile(
        r"^\s*(?P<name>int_flash(?:_config|_interrupts)?|int_sram(?:_stack_c0|_results)?)\s+0x[0-9A-Fa-f]+\s+0x(?P<length>[0-9A-Fa-f]+)\b"
    )

    for line in text.splitlines():
        m = sec_re.match(line)
        if m:
            sec = m.group(1)
            size = int(m.group(3), 16)
            if sec in ("flash", "flash_config", "flash_interrupts", "intc_vector"):
                flash_used += size
            elif sec == "sram_data":
                sram_data += size
            elif sec == "sram_bss":
                sram_bss += size
            elif sec == "heap":
                heap_sz += size
            elif sec in ("sram_interrupts", "acfls_code_ram"):
                sram_extra += size
        else:
            # Try to read totals from Memory Configuration table
            m4 = memcfg_re.match(line)
            if m4:
                name = m4.group("name")
                length = int(m4.group("length"), 16)
                if name == "int_flash":
                    flash_total = length
                elif name == "int_sram":
                    sram_total = length
                elif name == "int_sram_stack_c0":
                    stack_total = length
            # Fallback to LENGTH = 0x... lines if present
            m1 = length_re_flash.match(line)
            if m1:
                flash_total = int(m1.group(1), 16)
            m2 = length_re_sram.match(line)
            if m2:
                sram_total = int(m2.group(1), 16)
            m3 = length_re_stack.match(line)
            if m3:
                stack_total = int(m3.group(1), 16)

    return {
        "flash_used": flash_used,
        "flash_total": flash_total,
        "sram_used": sram_data + sram_bss + heap_sz + sram_extra,
        "sram_total": sram_total,
        "stack_total": stack_total,
    }


def main():
    if len(sys.argv) < 2:
        print("mapreport: usage: mapreport.py <file.map>", file=sys.stderr)
        sys.exit(2)
    map_path = Path(sys.argv[1])
    if not map_path.exists():
        print(f"mapreport: map file not found: {map_path}", file=sys.stderr)
        sys.exit(1)
    text = map_path.read_text(errors="ignore")
    stats = parse_map_text(text)

    def kb(x):
        return x / 1024.0

    def pct(used, total):
        if not total:
            return "n/a"
        return f"{(used * 100.0 / total):.1f}%"

    print(f"Memory Usage Summary ({map_path})")
    print(
        f"- Flash: {kb(stats['flash_used']):.2f} KB used / {kb(stats['flash_total']):.2f} KB total"
        f" ({pct(stats['flash_used'], stats['flash_total'])})"
    )
    print(
        f"- RAM  : {kb(stats['sram_used']):.2f} KB used / {kb(stats['sram_total']):.2f} KB total (stack excl.)"
        f" ({pct(stats['sram_used'], stats['sram_total'])})"
    )
    print(f"- Stack: {kb(stats['stack_total']):.2f} KB")


if __name__ == "__main__":
    main()
